Skip to content

Add API-backed liked items profile screen - #458

Merged
CherryCIC merged 7 commits into
mainfrom
cherry/liked-items-screen-clean
Jul 27, 2026
Merged

Add API-backed liked items profile screen#458
CherryCIC merged 7 commits into
mainfrom
cherry/liked-items-screen-clean

Conversation

@CherryCIC

@CherryCIC CherryCIC commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a profile-accessible Liked Items screen and makes the existing cherry product API the source of truth for likes.

This PR:

  • adds LikedItemsPage from the Profile Liked shortcut
  • displays liked products with the existing ProductCard component and links cards to the existing product page
  • loads likes through authenticated GET /api/products/my-liked-items
  • sends both like and unlike changes through POST /api/products/{id}/like with { "like": true|false }
  • removes direct client reads and writes to users/{uid}/likedProducts and removes Firestore product snapshot hydration
  • retains a small in-memory product cache so successful like actions update the current UI immediately
  • keeps Orders and Listings visible on Profile as requested

Why

The first implementation persisted and hydrated likes directly through Firestore. The backend now exposes dedicated authenticated endpoints for this flow. Using those endpoints keeps ownership of liked-item data in the backend and prevents the app from maintaining a second client-side Firestore data contract.

This also removes the mismatch where homepage products came from the API but the liked screen attempted to rebuild them from Firestore product documents.

Change type

UI, API/data-flow and test changes. No backend endpoint, Firestore rule or dependency changes are included.

Reviewer notes and risks

  • The existing DioApiService supplies the Firebase bearer token. This repository no longer accesses FirebaseAuth or FirebaseFirestore directly.
  • Like state changes are accepted locally only when the API returns success: true and confirms the requested data.liked value.
  • Invalid product IDs are rejected before a request is sent.
  • Malformed individual product records are skipped rather than crashing the full liked-items page.
  • The page requests the API maximum of 50 products. Cursor-based loading beyond the first 50 liked products is not included in this MVP change.
  • A successful response without data.products is treated as an empty result. Reviewers should confirm this fallback is preferred over an error state.
  • Orders and Listings remain visible no-op shortcuts because their destinations are outside the liked-items scope.
  • The branch includes the latest main, including product search and pagination from Add product search and pagination #461.

Testing

Passed:

  • flutter analyze
  • flutter test test/product_repository_test.dart test/liked_items_view_model_test.dart test/liked_items_page_test.dart test/home_screen_mvp_test.dart (27 tests)

The full flutter test run completed with 57 passing and 2 failing tests. Both failures are unchanged from origin/main and outside this PR scope:

  • test/donation_model_test.dart expects postage_size == medium, while current serialisation returns no postage_size field
  • test/settings_legal_information_test.dart expects Date last updated: 2024-13-11, which is not present in the rendered document

Not completed:

  • signed-in end-to-end testing against the live backend
  • post-migration Android or iOS device testing
  • a user account with more than 50 liked products

Existing UI evidence

These Android emulator screenshots were captured before the API migration. They demonstrate the screen and navigation only, not the new backend integration.

Profile liked shortcut Liked items flow Liked items screen Liked items product cards

Related work

Summary by CodeRabbit

  • New Features
    • Added a liked-items page (profile entry + /liked-items route) with loading, empty, and error/retry states, including browse CTA.
    • Enabled like/unlike from product cards with accessibility labels, plus safer like actions.
    • Added liked-items product grid and updated profile shortcut tiles to support navigation.
    • Added placeholder image fallback when products have no images.
  • Bug Fixes
    • Improved liked-items loading, de-duplication, and error handling; like/unlike updates now reflect asynchronously with proper state recovery.
  • Tests
    • Added widget and view-model tests for liked-items, plus expanded repository and profile-related test coverage.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e38b915-ba8a-4151-bf33-97225bdf01cc

📥 Commits

Reviewing files that changed from the base of the PR and between ba200dd and ea39364.

📒 Files selected for processing (2)
  • lib/core/config/app_strings.dart
  • lib/core/utils/dependency.dart
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/core/config/app_strings.dart
  • lib/core/utils/dependency.dart

📝 Walkthrough

Walkthrough

Adds an API-backed liked-items feature with asynchronous like/unlike state, cached products, a new page and route, profile navigation, product-card updates, and coverage for repository, view-model, page, and profile behavior.

Changes

Liked items

Layer / File(s) Summary
Liked-product API and dependency contracts
lib/core/config/firestore_constants.dart, lib/core/services/network/api_endpoints.dart, lib/features/products/product_repository.dart, lib/core/utils/dependency.dart
Adds liked-product endpoints, repository fetch/update operations, response validation, product ID validation, constants, and injected ApiService wiring.
Like state and product-card actions
lib/features/products/product_viewmodel.dart, lib/features/products/product_card.dart
Makes like updates asynchronous, tracks pending changes and cached snapshots, supports callbacks, adds accessibility labels, and handles missing product images.
Liked-items page and profile navigation
lib/core/config/app_strings.dart, lib/core/router/nav_routes.dart, lib/features/liked_items/*, lib/features/profile/...
Adds the liked-items route and page with loading, empty, error, and loaded states, product navigation, unlike actions, bottom navigation, and profile shortcut wiring.
Repository API validation
test/product_repository_test.dart
Tests liked-product fetching, parsing, deduplication, like/unlike requests, invalid IDs, and API failures.
Liked-items and profile validation
test/liked_items_*, test/mvp_profile_stats_test.dart, test/home_screen_mvp_test.dart, test/support/unexpected_api_service.dart
Tests view-model state transitions, cached products, page rendering, retry and unlike behavior, navigation, profile shortcuts, and dependency wiring.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • Cherry-CIC/MVP#443 — Overlaps with feature-flagged profile shortcut UI wiring.
  • Cherry-CIC/MVP#460 — Strongly overlaps with the liked-items page, view model, route, and like/unlike integration.
  • Cherry-CIC/MVP#465 — Shares profile shortcut callback changes and API endpoint updates.

Suggested reviewers: mattgoodwin0, yusuphjoluwasen

Sequence Diagram(s)

sequenceDiagram
  participant ProfilePage
  participant AppRoutes
  participant LikedItemsPage
  participant LikedItemsViewModel
  participant ProductRepository
  ProfilePage->>AppRoutes: Navigate to /liked-items
  AppRoutes->>LikedItemsPage: Build page
  LikedItemsPage->>LikedItemsViewModel: Load liked products
  LikedItemsViewModel->>ProductRepository: Fetch liked products
  ProductRepository-->>LikedItemsViewModel: Return products or error
  LikedItemsViewModel-->>LikedItemsPage: Render loading, empty, error, or loaded state
Loading
sequenceDiagram
  participant ProductCard
  participant ProductViewModel
  participant ProductRepository
  participant ApiService
  ProductCard->>ProductViewModel: Toggle like
  ProductViewModel->>ProductRepository: Like or unlike product
  ProductRepository->>ApiService: POST product like endpoint
  ApiService-->>ProductRepository: Return response
  ProductRepository-->>ProductViewModel: Return Result<bool>
  ProductViewModel-->>ProductCard: Update like state
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an API-backed liked items screen in Profile.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cherry/liked-items-screen-clean

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@CherryCIC CherryCIC self-assigned this Jul 19, 2026
@CherryCIC CherryCIC added this to the User Profile milestone Jul 19, 2026
@CherryCIC
CherryCIC marked this pull request as ready for review July 19, 2026 23:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75e9aa6a82

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +110 to +114
_productFromLikedDocument(
likedDocument.data(),
reference.productId,
) ??
await _fetchProduct(reference.productId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate snapshots before showing liked items

When a liked document contains productSnapshot, this path short-circuits the live product fetch, so the _isHiddenProduct check is never applied. Because likeProduct writes a snapshot for every new like, a user who liked an item before it was archived or deleted will still see and be able to open the stale item from Liked Items; fetch the live product/status first or validate the snapshot against current product state before returning it.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
lib/features/products/product_repository.dart (2)

86-96: 🚀 Performance & Scalability | 🔵 Trivial

Consider capping/paginating the liked-items query for scalability.

fetchLikedProducts loads the entire likedProducts subcollection with no limit(). For users with very large liked lists this grows unbounded read cost/latency on every page load; consider a limit() with incremental pagination as the feature scales.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/products/product_repository.dart` around lines 86 - 96, Update
fetchLikedProducts to avoid loading the entire likedProducts subcollection in
one request by applying a bounded Firestore limit and supporting incremental
pagination with a cursor. Preserve the existing likedAt descending order and
product conversion behavior, and expose or reuse the surrounding pagination
contract so callers can request subsequent pages.

92-136: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Sequential per-item fetches and lost ordering for fallback-recovered products.

Two related issues in this loop:

  1. For every liked document without a usable embedded snapshot, await _fetchProduct(reference.productId) (Line 114) runs sequentially inside the for loop — N missing snapshots means N serial network round trips before the batched home-feed fallback even starts.
  2. Products recovered via _fetchProductsFromHomeFeed are appended in a second pass (Lines 124-130) after the main loop, so they lose their original likedAt-descending position — the final list is no longer fully ordered by like time whenever any items needed the fallback path.
♻️ Suggested restructure (parallel fetch + order-preserving assembly)
-      final products = <Product>[];
-      final missingProductIds = <String>[];
-      final seenProductIds = <String>{};
-
-      for (final likedDocument in likedSnapshot.docs) {
-        final reference = LikedProductReference.tryParse(
-          documentId: likedDocument.id,
-          data: likedDocument.data(),
-        );
-        if (reference == null) {
-          continue;
-        }
-
-        final product =
-            _productFromLikedDocument(
-              likedDocument.data(),
-              reference.productId,
-            ) ??
-            await _fetchProduct(reference.productId);
-        if (product != null) {
-          if (seenProductIds.add(product.id)) {
-            products.add(product);
-          }
-        } else {
-          missingProductIds.add(reference.productId);
-        }
-      }
-
-      final fallbackProducts = await _fetchProductsFromHomeFeed(missingProductIds);
-      for (final productId in missingProductIds) {
-        final product = fallbackProducts[productId];
-        if (product != null && seenProductIds.add(product.id)) {
-          products.add(product);
-        }
-      }
-
-      return Result.success(products);
+      final docs = likedSnapshot.docs;
+      final resolved = List<Product?>.filled(docs.length, null);
+      final pendingIndices = <int>[];
+      final pendingIds = <String>[];
+
+      for (var i = 0; i < docs.length; i++) {
+        final reference = LikedProductReference.tryParse(
+          documentId: docs[i].id,
+          data: docs[i].data(),
+        );
+        if (reference == null) continue;
+
+        final snapshotProduct = _productFromLikedDocument(docs[i].data(), reference.productId);
+        if (snapshotProduct != null) {
+          resolved[i] = snapshotProduct;
+        } else {
+          pendingIndices.add(i);
+          pendingIds.add(reference.productId);
+        }
+      }
+
+      final fetched = await Future.wait(pendingIds.map(_fetchProduct));
+      final stillMissing = <String>[];
+      for (var j = 0; j < pendingIndices.length; j++) {
+        resolved[pendingIndices[j]] = fetched[j];
+        if (fetched[j] == null) stillMissing.add(pendingIds[j]);
+      }
+
+      final fallbackProducts = await _fetchProductsFromHomeFeed(stillMissing);
+      for (var j = 0; j < pendingIndices.length; j++) {
+        final idx = pendingIndices[j];
+        resolved[idx] ??= fallbackProducts[pendingIds[j]];
+      }
+
+      final seenProductIds = <String>{};
+      final products = <Product>[
+        for (final product in resolved)
+          if (product != null && seenProductIds.add(product.id)) product,
+      ];
+
+      return Result.success(products);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/products/product_repository.dart` around lines 92 - 136,
Refactor the liked-products assembly in the repository method containing the
shown loop to avoid awaiting _fetchProduct sequentially: collect unresolved
references and fetch them concurrently, while retaining each liked document’s
position. Then resolve fallback entries through _fetchProductsFromHomeFeed and
assemble the final products in the original likedAt-descending iteration order,
using seenProductIds for deduplication and preserving the existing Result
behavior.
lib/features/products/product_viewmodel.dart (1)

100-121: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use value equality for liked-product snapshots. _likedProductSnapshots[product.id] != product is identity-based here because Product doesn’t define ==/hashCode. Since loadLikedProducts() passes freshly fetched Product instances into cacheLikedProducts(), unchanged items still look different and notifyListeners() fires on each refresh. Compare a stable field or add value equality to Product.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/products/product_viewmodel.dart` around lines 100 - 121, Update
cacheLikedProducts to compare liked-product snapshots by stable product values
rather than Product identity, so freshly fetched but unchanged products do not
set changed or trigger notifyListeners; use an existing stable field comparison
or implement Product value equality via ==/hashCode.
test/liked_product_reference_test.dart (1)

1-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a populated likedAt value.

All three tests omit likedAt from the input data, so _parseLikedAt's Timestamp/DateTime branches are never exercised — only the null case is covered. Since likedAt is a core part of the persisted schema this PR introduces, add a case asserting successful parsing of a real value (e.g., Timestamp.fromDate(...)).

✅ Suggested additional test
+    test('parses a populated likedAt timestamp', () {
+      final likedAt = DateTime(2026, 1, 1);
+      final reference = LikedProductReference.tryParse(
+        documentId: 'product-4',
+        data: {'productId': 'product-4', 'likedAt': Timestamp.fromDate(likedAt)},
+      );
+
+      expect(reference, isNotNull);
+      expect(reference!.likedAt, likedAt);
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/liked_product_reference_test.dart` around lines 1 - 36, The
LikedProductReference tests lack coverage for parsing a populated likedAt value.
Add a test in the LikedProductReference group that passes a real
Timestamp.fromDate value through tryParse, asserts the reference is created, and
verifies likedAt matches the expected parsed DateTime.
test/mvp_profile_stats_test.dart (1)

93-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for Profile → Liked Items navigation.

This test confirms the "Liked" tile renders but never taps it to verify it actually navigates to LikedItemsPage, which is the PR's core new user flow. Mirror the routing setup from test/liked_items_page_test.dart (onGenerateRoute: AppRoutes.generateRoute + navigatorKey) and assert find.byType(LikedItemsPage) after tapping the "Liked" tile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/mvp_profile_stats_test.dart` around lines 93 - 119, The ProfilePage
widget test currently verifies only that the Liked shortcut renders, not that it
navigates correctly. Update the test setup around ProfilePage to mirror
liked_items_page_test.dart by configuring AppRoutes.generateRoute and the shared
navigatorKey, then tap the profileUserLiked tile and assert that LikedItemsPage
is present while preserving the existing visibility assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/features/liked_items/liked_items_page.dart`:
- Line 136: Update the onLikePressed callback in the liked-items page to await
unlikeProduct’s boolean result, and show a SnackBar when it returns false so the
view model’s errorMessage is surfaced to the user. Preserve the existing
successful unlike behavior and use the page’s current BuildContext and
established error-message access.

In `@lib/features/liked_items/liked_items_view_model.dart`:
- Around line 55-74: Update unlikeProduct to avoid reusing the pre-await index
after setProductLiked completes: locate the product by id against the current
_products when applying the successful removal, preferably filtering out
product.id. Preserve the existing success/error state updates and safely handle
concurrent unlikeProduct calls without removeAt index failures.

---

Nitpick comments:
In `@lib/features/products/product_repository.dart`:
- Around line 86-96: Update fetchLikedProducts to avoid loading the entire
likedProducts subcollection in one request by applying a bounded Firestore limit
and supporting incremental pagination with a cursor. Preserve the existing
likedAt descending order and product conversion behavior, and expose or reuse
the surrounding pagination contract so callers can request subsequent pages.
- Around line 92-136: Refactor the liked-products assembly in the repository
method containing the shown loop to avoid awaiting _fetchProduct sequentially:
collect unresolved references and fetch them concurrently, while retaining each
liked document’s position. Then resolve fallback entries through
_fetchProductsFromHomeFeed and assemble the final products in the original
likedAt-descending iteration order, using seenProductIds for deduplication and
preserving the existing Result behavior.

In `@lib/features/products/product_viewmodel.dart`:
- Around line 100-121: Update cacheLikedProducts to compare liked-product
snapshots by stable product values rather than Product identity, so freshly
fetched but unchanged products do not set changed or trigger notifyListeners;
use an existing stable field comparison or implement Product value equality via
==/hashCode.

In `@test/liked_product_reference_test.dart`:
- Around line 1-36: The LikedProductReference tests lack coverage for parsing a
populated likedAt value. Add a test in the LikedProductReference group that
passes a real Timestamp.fromDate value through tryParse, asserts the reference
is created, and verifies likedAt matches the expected parsed DateTime.

In `@test/mvp_profile_stats_test.dart`:
- Around line 93-119: The ProfilePage widget test currently verifies only that
the Liked shortcut renders, not that it navigates correctly. Update the test
setup around ProfilePage to mirror liked_items_page_test.dart by configuring
AppRoutes.generateRoute and the shared navigatorKey, then tap the
profileUserLiked tile and assert that LikedItemsPage is present while preserving
the existing visibility assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ddd4646d-0675-4781-9486-790d1ea58088

📥 Commits

Reviewing files that changed from the base of the PR and between 31b2bdd and 75e9aa6.

📒 Files selected for processing (16)
  • lib/core/config/app_strings.dart
  • lib/core/config/firestore_constants.dart
  • lib/core/router/nav_routes.dart
  • lib/core/utils/dependency.dart
  • lib/features/liked_items/liked_items_page.dart
  • lib/features/liked_items/liked_items_view_model.dart
  • lib/features/liked_items/models/liked_product_reference.dart
  • lib/features/products/product_card.dart
  • lib/features/products/product_repository.dart
  • lib/features/products/product_viewmodel.dart
  • lib/features/profile/profile_page.dart
  • lib/features/profile/widgets/user_order_details.dart
  • test/liked_items_page_test.dart
  • test/liked_items_view_model_test.dart
  • test/liked_product_reference_test.dart
  • test/mvp_profile_stats_test.dart

key: ValueKey(product.id),
product: product,
onTap: () => context.read<ProductViewModel>().goToProductPage(product),
onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle the potential failure when unliking a product.

Currently, if unlikeProduct fails (e.g., due to a network error), it returns false and sets an errorMessage in the view model. However, because the page status remains LikedItemsStatus.loaded, the UI ignores this error silently and leaves the user wondering why the item wasn't removed.

Please await the result and display a SnackBar to inform the user if the action fails.

🐛 Proposed fix
-          onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product),
+          onLikePressed: () async {
+            final viewModel = context.read<LikedItemsViewModel>();
+            final success = await viewModel.unlikeProduct(product);
+            if (!success && context.mounted) {
+              ScaffoldMessenger.of(context).showSnackBar(
+                SnackBar(
+                  content: Text(viewModel.errorMessage ?? AppStrings.genericError),
+                ),
+              );
+            }
+          },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product),
onLikePressed: () async {
final viewModel = context.read<LikedItemsViewModel>();
final success = await viewModel.unlikeProduct(product);
if (!success && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(viewModel.errorMessage ?? AppStrings.genericError),
),
);
}
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/liked_items/liked_items_page.dart` at line 136, Update the
onLikePressed callback in the liked-items page to await unlikeProduct’s boolean
result, and show a SnackBar when it returns false so the view model’s
errorMessage is surfaced to the user. Preserve the existing successful unlike
behavior and use the page’s current BuildContext and established error-message
access.

Comment on lines +55 to +74
Future<bool> unlikeProduct(Product product) async {
final index = _products.indexWhere((item) => item.id == product.id);
if (index == -1) {
return true;
}

final result = await productViewModel.setProductLiked(product, false);
if (!result.isSuccess) {
_errorMessage = result.error ?? 'Unable to remove this liked item.';
notifyListeners();
return false;
}

final nextProducts = List<Product>.from(_products)..removeAt(index);
_products = List.unmodifiable(nextProducts);
_status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded;
_errorMessage = null;
notifyListeners();
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

TOCTOU: stale index reused against _products after an await, risking wrong-item removal or a crash.

index is computed from _products before the await productViewModel.setProductLiked(...) call, then reused against _products (which may have been reassigned in the meantime) to removeAt(index). Two near-simultaneous unlikeProduct calls on different items reproduce this without any external trigger:

  • Call A captures index=1 (item B) and awaits.
  • Call B captures index=2 (item C) and awaits, still against the original list.
  • Call A resolves, removes index 1 → _products now has 2 items.
  • Call B resolves and does removeAt(2) against the now-shorter _productsRangeError, or removes the wrong item if lengths still happen to align.
🐛 Suggested fix (filter by id at removal time instead of a captured index)
   Future<bool> unlikeProduct(Product product) async {
-    final index = _products.indexWhere((item) => item.id == product.id);
-    if (index == -1) {
+    final wasPresent = _products.any((item) => item.id == product.id);
+    if (!wasPresent) {
       return true;
     }

     final result = await productViewModel.setProductLiked(product, false);
     if (!result.isSuccess) {
       _errorMessage = result.error ?? 'Unable to remove this liked item.';
       notifyListeners();
       return false;
     }

-    final nextProducts = List<Product>.from(_products)..removeAt(index);
-    _products = List.unmodifiable(nextProducts);
+    _products = List.unmodifiable(
+      _products.where((item) => item.id != product.id),
+    );
     _status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded;
     _errorMessage = null;
     notifyListeners();
     return true;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<bool> unlikeProduct(Product product) async {
final index = _products.indexWhere((item) => item.id == product.id);
if (index == -1) {
return true;
}
final result = await productViewModel.setProductLiked(product, false);
if (!result.isSuccess) {
_errorMessage = result.error ?? 'Unable to remove this liked item.';
notifyListeners();
return false;
}
final nextProducts = List<Product>.from(_products)..removeAt(index);
_products = List.unmodifiable(nextProducts);
_status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded;
_errorMessage = null;
notifyListeners();
return true;
}
Future<bool> unlikeProduct(Product product) async {
final wasPresent = _products.any((item) => item.id == product.id);
if (!wasPresent) {
return true;
}
final result = await productViewModel.setProductLiked(product, false);
if (!result.isSuccess) {
_errorMessage = result.error ?? 'Unable to remove this liked item.';
notifyListeners();
return false;
}
_products = List.unmodifiable(
_products.where((item) => item.id != product.id),
);
_status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded;
_errorMessage = null;
notifyListeners();
return true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/liked_items/liked_items_view_model.dart` around lines 55 - 74,
Update unlikeProduct to avoid reusing the pre-await index after setProductLiked
completes: locate the product by id against the current _products when applying
the successful removal, preferably filtering out product.id. Preserve the
existing success/error state updates and safely handle concurrent unlikeProduct
calls without removeAt index failures.

@yusuphjoluwasen

yusuphjoluwasen commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks for working on this, please let's switch this to use the api backend to reduce out direct firestore logic, there is already an api for it.

https://cherry-backend-401854471349.europe-west2.run.app/api-docs/#/Products/get_api_products_my_liked_items

we also have an api for like/unlike items, i think it has been integrated already, but its good to confirm - if not we might need to integrate that as well
https://cherry-backend-401854471349.europe-west2.run.app/api-docs/#/Products/post_api_products__id__like

@CherryCIC

Copy link
Copy Markdown
Collaborator Author

Minor glitch where if you select the liked product from the list, then unlike it and go back to the screen, you will see the item unliked on the screen until you click off of it.

image

@CherryCIC CherryCIC changed the title Add Firestore-backed liked items profile screen Add API-backed liked items profile screen Jul 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lib/features/products/product_repository.dart (1)

22-66: 🚀 Performance & Scalability | 🔵 Trivial

Consider supporting pagination for liked items.

Currently, fetchLikedProducts hardcodes a limit of 50 items and does not process the pagination metadata (such as nextCursor) returned by the API. Users with more than 50 liked items will only see their 50 most recent likes.

As the feature matures and user activity grows, consider updating this method to accept a cursor and returning the pagination metadata alongside the products, enabling the UI to implement infinite scrolling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/features/products/product_repository.dart` around lines 22 - 66, The
fetchLikedProducts method currently retrieves only one fixed-size page and
discards pagination metadata. Update its API and implementation to accept an
optional cursor, pass that cursor with the existing limit to
ApiEndpoints.likedProducts, and return the extracted products together with the
response’s nextCursor (using the project’s established pagination result type),
so callers can request subsequent pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/features/products/product_repository.dart`:
- Around line 22-66: The fetchLikedProducts method currently retrieves only one
fixed-size page and discards pagination metadata. Update its API and
implementation to accept an optional cursor, pass that cursor with the existing
limit to ApiEndpoints.likedProducts, and return the extracted products together
with the response’s nextCursor (using the project’s established pagination
result type), so callers can request subsequent pages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ed6fea7c-2d68-4ada-af71-069a73affc1c

📥 Commits

Reviewing files that changed from the base of the PR and between 75e9aa6 and f89de33.

📒 Files selected for processing (10)
  • lib/core/config/firestore_constants.dart
  • lib/core/services/network/api_endpoints.dart
  • lib/core/utils/dependency.dart
  • lib/features/products/product_repository.dart
  • lib/features/products/product_viewmodel.dart
  • test/home_screen_mvp_test.dart
  • test/liked_items_page_test.dart
  • test/liked_items_view_model_test.dart
  • test/product_repository_test.dart
  • test/support/unexpected_api_service.dart
💤 Files with no reviewable changes (1)
  • lib/core/config/firestore_constants.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/features/products/product_viewmodel.dart
  • test/liked_items_page_test.dart
  • test/liked_items_view_model_test.dart

@CherryCIC

Copy link
Copy Markdown
Collaborator Author

@yusuphjoluwasen thank you for your advice, I have since made changes that use the api backend so we may reduce our direct firestore logic.

The liked-items flow now:
Uses authenticated GET /api/products/my-liked-items.
Uses POST /api/products/{id}/like for both liking and unliking.
Removes direct liked-item Firestore reads, writes and product snapshots.
Retains an in-memory cache for immediate UI updates.
Includes focused API contract and failure tests.

Validation:
flutter analyze: passed.
Focused suite: 27 tests passed.
Full suite: 57 passed, 2 unrelated baseline failures.
GitHub CodeQL: passed.
CodeRabbit: pending.
PR remains seemingly mergeable.

@VitaliiKoliuka

Copy link
Copy Markdown
Contributor

Manual testing result

Environment

Liked Items API flow — Failed

Steps

  1. Logged in with an existing test account.
  2. Opened Home.
  3. Liked a product.
  4. Confirmed that the heart state changed immediately.
  5. Opened Profile → Liked.

Expected result
User see the liked product on the Liked Items screen.

Actual result
User see a Retry button and the message:

Server is currently unavailable. Please try again later.

The immediate heart-state update worked, but the liked product could not be loaded through the live API flow.

Evidences

like server unavailaible LOGS.txt

Screenshot_20260721_202402

Logout flow — Retest passed

During an earlier test, the app returned to Profile after logout and Home displayed “No products available.”

I repeated the logout flow, but the issue did not reproduce:

  • logout completed successfully;
  • the app returned to the expected unauthenticated screen;
  • the earlier Home/Profile issue was not observed.

I am therefore treating the earlier result as an intermittent, currently unconfirmed observation.

Automated tests

flutter test result:

  • 57 tests passed
  • 2 tests failed

The failures match the documented unrelated baseline failures:

  • test/donation_model_test.dart
  • test/settings_legal_information_test.dart

Could you please confirm whether GET /api/products/my-liked-items is deployed and available in the environment used by the app?

@yusuphjoluwasen

yusuphjoluwasen commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Please retest @VitaliiKoliuka, The issue was from firebase.

@VitaliiKoliuka

Copy link
Copy Markdown
Contributor

Retest blocked by flutter analyze failure

Environment

After updating the PR branch and merging the latest main, I ran:

flutter analyze

Result: Failed

error - The named parameter 'apiService' is required, but there's no corresponding argument.
test/mvp_profile_stats_test.dart:25:10 - missing_required_argument

1 issue found.

@VitaliiKoliuka

Copy link
Copy Markdown
Contributor

Device retest — liked state is not restored and like count is double-counted

Environment

I reproduced an inconsistency between the current user’s liked state and the displayed like counter.

Steps to reproduce

  1. Log in with a test account.
  2. Open Home.
  3. Tap the heart once on the product.
  4. Confirm that the heart becomes red and the counter changes from 0 to 1.
  5. Open Profile → Liked.
  6. Observe the same product.
  7. Log out, restart/reopen the app, and log in again.
  8. Open Home.
  9. Open Profile → Liked.
  10. Return to Home.

Actual result

  • Immediately after one like action, Home correctly displayed a red heart with counter 1.
  • In Profile → Liked, the same single product displayed a red heart with counter 2.
  • The product card itself was not duplicated.
  • After logging in again, Home displayed an outlined heart with counter 1, although the product remained liked by the current user.
  • After opening Profile → Liked, the product displayed a red heart with counter 2.
  • Returning to Home then changed Home to a red heart with counter 2.

Expected result

  • After one like action on a product starting at 0, the product should display a red heart with counter 1.
  • The current user’s liked state should be restored after restarting or logging in again.
  • Home and Profile → Liked should display the same heart state and counter.
  • Opening the Liked Items screen should not increase the displayed like counter.

Evidence

video.liked.state.mp4

@VitaliiKoliuka

Copy link
Copy Markdown
Contributor

Please retest @VitaliiKoliuka, The issue was from firebase.

Thank you, @yusuphjoluwasen. I retested , issue was resolved.

The previous “Server is currently unavailable” error did not reproduce, and Profile → Liked loaded successfully.

@yusuphjoluwasen yusuphjoluwasen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good

@CherryCIC
CherryCIC merged commit 033379c into main Jul 27, 2026
3 checks passed
@CherryCIC

CherryCIC commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author
IMG_2035.mov

Thank you @VitaliiKoliuka for the review and for re-checking!

It seems to be working on my end and with the approval of @yusuphjoluwasen and your recent successful test, the PR can be merged with confidence.

Thank you both very much; your hard work, contributions and support are greatly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants